/*
 * WorxXMLRequest.java
 *
 * Created Apr 27, 2006
 * David Connell
 * Copyright (c) 2005 Synergy Systems, Inc
 */
package org.puresilk.android.net;

// java support
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.zip.GZIPInputStream;

import org.puresilk.android.util.Logger;
import org.puresilk.android.util.MultiPartForm;

// android support
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;

// Worx support.

/**
 * Request manager for making HTTP Post calls from
 * mobile platforms.  Uses an async loop to read 
 * data and pass to delegate (Response).  Handles
 * String (HTML), XML and JSON requests.
 *
 */
public class HttpRequest {
	private Logger logger = Logger.getInstance("WorxRequest");
	
	// request / response type constants (default = String)
	public static final int DEFAULT_REQUEST_TYPE = 0;
	public static final int JSON_REQUEST_TYPE = 1;
	public static final int XML_REQUEST_TYPE = 2;

	// read buffer size
	private static final int MAX_BUFFER_SIZE = (1024 * 1024) * 2;  // 2MB		
	private static final int DEFAULT_BUFFER_SIZE = 1024 * 5;  // 5KB

	// Default HTTP request mode
	private static final String DEFAULT_OP = "POST";
	
	// states
	public static final int NEW = 0;
	public static final int READING = 1;
	public static final int DONE = 2;
	public static final int ERROR = -1;

	// attributes
	private String _url = null;
	private String _data = null;
	private String _httpOp = DEFAULT_OP;
	private int _type = DEFAULT_REQUEST_TYPE;;
	private int _bfsz = DEFAULT_BUFFER_SIZE;
	private int _state = NEW;
	private boolean _multiPart = false;
	private MultiPartForm mpf = null;
	
	//----------------------------------
	// Static submission and wrappers 
	//----------------------------------

	/**
	 * One stop shopping for making requests,
	 * @param url
	 * @param data / mpf (query string or MultiPartForm)
	 * @param rType
	 * @return
	 * @throws HttpException
	 */
	public static HttpResponse submit(String url, String data, int rType, Context ctx, Handler handler)  throws HttpException {
		HttpRequest wxr = new HttpRequest(url,data,rType);
		HttpResponse rsp = wxr.fetch(ctx, handler); 
		return rsp;
	}
	
	public static HttpResponse jsonFetch(String url, String data, Context ctx, Handler handler) throws HttpException {
		return submit(url, data, JSON_REQUEST_TYPE, ctx, handler);
	}
	
	public static HttpResponse xmlFetch(String url, String data, Context ctx, Handler handler) throws HttpException {
		return submit(url, data, XML_REQUEST_TYPE, ctx, handler);
	}
	
	public static HttpResponse submit(String url, MultiPartForm mpf, int rType, Context ctx, Handler handler) throws HttpException {
		HttpRequest wxr = new HttpRequest(url,mpf,rType);
		HttpResponse rsp = wxr.fetch(ctx, handler); 
		return rsp;
	}

	public static HttpResponse jsonFetch(String url, MultiPartForm mpf, Context ctx, Handler handler) throws HttpException {
		return submit(url, mpf, JSON_REQUEST_TYPE, ctx, handler);
	}
	
	public static HttpResponse xmlFetch(String url, MultiPartForm mpf, Context ctx, Handler handler) throws HttpException {
		return submit(url, mpf, XML_REQUEST_TYPE, ctx, handler);
	}
	
	//------------------------------
	// implementation
	
	/**
	 * MultiPart Form 
	 */
	
	public boolean isMultiPart(){
		return _multiPart;
	}
	
	public void setMultiPart(MultiPartForm frm){
		_multiPart = true;
		mpf = frm;
	}
	
	/**
	 * set the transfer buffer size
	 * @param bfsz
	 */
	public void setBufferSize(int bfsz) {
		_bfsz = bfsz > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : bfsz;
	}
	
	/**
	 * Check transfer buffer size
	 * @return
	 */
	public int getBufferSize() {
		return _bfsz;
	}

	/**
	 * Check the status of the thread 
	 */
	public int getStatus() { return _state; }
	
	/** Default Constructor */
	public HttpRequest() {
		
	}
	
	public HttpRequest(String url, MultiPartForm mpf){
		_url = url;
		setMultiPart(mpf);
	}
	
	public HttpRequest(String url, MultiPartForm mpf, int rType){
		_url = url;
		setMultiPart(mpf);
		_type = rType;
	}
	
	/** semi complete constructor */ 
	public HttpRequest(String url, String data) {
		_url = url;
		_data = data;
	}
	/** usable constructor */
	public HttpRequest(String url, String data, int rType) {
		_url = url;
		_data = data;
		_type = rType;
	}
	
	/** usable constructor */
	public HttpRequest(String url, String data, int rType, String op) {
		_url = url;
		_data = data;
		_type = rType;
		_httpOp = op;
	}
	
	/**
	 * Post the request to server via HTTP and send response
	 * tracking back to delegate as things progress.
	 * 
	 * @param dlgt
	 */
	public synchronized void post(HttpResponseDelegate dlgt) {
		boolean good = false;
		int cnter = 0;
		
		while(!good){
			try {
				_transmit(dlgt);
				good = true;
			}
			catch(Exception ex){
				logger.debug("Network Retry #"+cnter);
				cnter++;
				good = false;
				if (cnter == 2) {
					// flag and notify 
					_state = ERROR;
					dlgt.onReadError(ex);
					//break out!
					good = true;
				}
			}
		}
	}
		
	protected synchronized void _transmit(HttpResponseDelegate dlgt) throws Exception {
		byte [] buffer = new byte[_bfsz];
		int cnt = _bfsz;
		InputStream isr = null;
		OutputStream os = null;
		HttpURLConnection uc = null;
		int bytesExpected = 0;
		int bytesRead = 0;
		
		// a bit of logging
		logger.debug("[" + _httpOp + "] "  + _url + "?" + _data );
			
		try {
			// first check for valid method (just post and get for now)
			if (!_httpOp.equals("POST") && !_httpOp.equals("GET") )
				throw new HttpException("Unsupported Method (" + _httpOp +")");
			
			// prepare the request
			URL reqUrl = new URL(_url);
			
			// notify delegate
			dlgt.onReadyToConnect();
			
			// get the connection
			uc = (HttpURLConnection)reqUrl.openConnection();
			uc.setRequestMethod(_httpOp);

			// prepare for reading and set timeouts in ms
			uc.setConnectTimeout(15000);
			uc.setReadTimeout(20000);
			uc.setRequestProperty("Accept-Encoding", "gzip");
			uc.setRequestProperty("Connection", "Keep-Alive");
			
			// Posting data perhaps?
			if (DEFAULT_OP.equals(_httpOp.toUpperCase())) {
				// open for posting
				uc.setDoOutput(true);
				
				// Using MutliPartForm?
				if (_multiPart) {
					// Yes, thank you
					// set properties
					uc.setRequestProperty("Content-Type", mpf.getContentType());
					uc.setRequestProperty("Content-Length", String.valueOf(mpf.getContentSize()));
					
					// FYI
					logger.info(String.format("MPF:: Content Length: %d", mpf.getContentSize()));
					
					// send output
					uc.connect();
					os = uc.getOutputStream();
					mpf.write(os, dlgt);
					os.close();
				}
				else {
					// No thanks, encoding for just the values please...
					byte [] data = encodeParams(_data);

					// FYI
					logger.info(String.format("POST:: Content Length: %d", data.length));

					// send output
					uc.connect();
					os = uc.getOutputStream();
					os.write(data);
					os.close();
				}
			}
			else 
				// nope, just connect and retrieve
				uc.connect();
			
			String encoding = uc.getContentEncoding();
			logger.debug("Content Encoding = "+ encoding);
			
			if (encoding != null && encoding.equals("gzip")) {
				isr = new GZIPInputStream(uc.getInputStream());
			} else {
				isr = uc.getInputStream();
			}
			
			// alert delegate
			_state = READING;
			
			bytesExpected = uc.getContentLength();
			// data read and transfer loop
			do {
				if (bytesRead == 0) {
					dlgt.onReadyToRead(uc);
					try {
						logger.info("Bytes Expected: " + Integer.toString(bytesExpected));
						} catch (Exception ex) {}
					}
				
				// read in data
				cnt = isr.read(buffer, 0, _bfsz);
				bytesRead += cnt;
				
				if (bytesRead <= 0) {
					throw new Exception("Timeout: No data read");
				}
				
				if (cnt > 0) {
					//logger.debug(buffer.toString());
					dlgt.onDataRead(buffer,cnt);
				}

			} while (cnt != -1);
			
			// all done reading
			_state = DONE;

			// alert delegate
			dlgt.onFinishedReading();						
		}
		catch(Exception ex){
			logger.error("Thrown Exception");
			throw new Exception(ex);
		}
		finally {
			// clean up
			try {isr.close();} catch(Exception e) {}
			try {uc.disconnect();} catch(Exception e) {}
		}
	}

	/**
	 * URL Encode just the value portions of
	 * query string.
	 * 
	 * @param data
	 * @return
	 */
	private byte[] encodeParams(String data) {
		StringBuilder sb = new StringBuilder();
		String [] nvp = data.split("&");
		
		// pairs loop
		for(int i=0; i< nvp.length; i++){
			// parse n,p
			String [] p = nvp[i].split("=");
			if (p.length < 2) {
				break;
			}
			
			// now put it back together nicely
			if (sb.length() > 0)
				sb.append("&");
			sb.append(p[0]).append("=").append(URLEncoder.encode(p[1]));	
		}			
		logger.info("Encoded params: " + sb.toString());
		
		// done
		return sb.toString().getBytes();
	}
	
	/**
	 * Connect to server, submit request and
	 * return the (asynchronous) response object.
	 * 
	 * Note:
	 * The network response is processed in an
	 * async loop so the response status needs to 
	 * be checked before processing.
	 * 
	 *     if (rsp.ready()) {
	 *        // process
	 *        ...
	 *     }
	 *     
	 *     - Error Check -
	 *     if ( rsp.getResultCode() != READING_ERROR) {
	 *        // process
	 *        ...
	 *     {
	 *      
	 * @return
	 * @throws HttpException
	 */
	public HttpResponse fetch(Context ctx,Handler handler) throws HttpException {
		logger.info("Request: " + _url);

		// check network status and will
		// throw WME if not able to find
		// and connect to server.
		_verifyConnectivity(ctx);
				
		// Target response
		final HttpResponse rsp = new HttpResponse(_type);
		rsp.registerCaller(handler);

		// create thread and post request...
		Thread bkgThread = new Thread (
			new Runnable(){
				public void run(){
					post(rsp);
				}
			}
		);
		// roll 'em
		bkgThread.start();
		
		// send back response
		return rsp;
	}
	
	/** Wrapper to submit JSON request */
	public HttpResponse fetchJSON(Context ctx,Handler handler) throws HttpException {
		setType(JSON_REQUEST_TYPE);
		return fetch(ctx,handler);
	}

	/** Wrapper to submit XML request */
	public HttpResponse fetchXML(Context ctx,Handler handler) throws HttpException {
		setType(XML_REQUEST_TYPE);
		return fetch(ctx, handler);
	}
	
	// -- Implementation --
	/**
	 * Validate an active network connection
	 */
	private void _verifyConnectivity(Context ctx) throws HttpException {
		ConnectivityManager cm = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
		NetworkInfo ni = cm.getActiveNetworkInfo();
		if( (ni==null) || !ni.isAvailable() || !ni.isConnected()){
			throw new HttpException("Network is not available.");
		}
	}
	
	/**
	 * Raw text getter
	 * @return
	 */
	public String getData() {
		return _data;
	}

	public void setData(String _data) {
		this._data = _data;
	}

	/** URL Getter */
	public String getUrl() {
		return _url;
	}

	/** URL Setter */
	public void setUrl(String _url) {
	  	this._url = _url;
	}

	/** Expected return type getter */
	public int getType() {
		return _type;
	}

	/** Expected return type setter */
	public void setType(int _type) {
		this._type = _type;
	}

	/**
	 * @return the _httpOp
	 */
	public String getHttpOp() {
		return _httpOp;
	}

	/**
	 * @param _httpOp the _httpOp to set
	 */
	public void setHttpOp(String _httpOp) {
		this._httpOp = _httpOp;
	}
}
